1. Wprowadzenie

W ramach niniejszej analizy prognozujemy ceny ropy Brent przy użyciu różnych metod modelowania szeregów czasowych.

ARIMA (AutoRegressive Integrated Moving Average) to klasyczny model statystyczny używany do analizy i prognozowania danych szeregów czasowych.

Model ARIMA łączy trzy komponenty:

Prophet to model opracowany przez Facebooka, zaprojektowany specjalnie z myślą o prognozowaniu danych z wyraźną sezonowością dzienną, tygodniową i roczną. Prophet jest odporny na brakujące dane i nietypowe wartości (outliery) oraz umożliwia uwzględnianie dodatkowych zmiennych wyjaśniających (regresorów) oraz świąt (holidays).

Komponenty Propheta
Komponenty Propheta

W projekcie wykorzystano m.in. modele Prophet, Prophet z dodatkowymi zmiennymi, ARIMA oraz KNN. Oceniono ich dokładność i przeprowadzono predykcję na przyszłe 30 dni.


2. Pobranie danych

brent_prices <- tq_get("BZ=F")
brent_prices %>% plot_time_series(date, close, .smooth = T,
                                  .interactive = T,
                                  .title = "Brent Oil price (USD)")

3. Dodanie zmiennych objaśniających

dolar_index <- tq_get("DX-Y.NYB")

brent_prices <- brent_prices %>% 
  mutate(date = as.Date(date)) %>%
  left_join(dolar_index %>% mutate(date = as.Date(date)), by = "date") %>%
  select(date, close = close.x, dxy = close.y) %>%
  drop_na()

4. Stacjonarność szeregu

adf.test(brent_prices$close, alternative = "stationary")

    Augmented Dickey-Fuller Test

data:  brent_prices$close
Dickey-Fuller = -2.3628, Lag order = 13, p-value = 0.4247
alternative hypothesis: stationary

5. Inżynieria cech: analiza techniczna

brent_prices <- brent_prices %>%
  arrange(date) %>%
  mutate(
    macd_full = MACD(close, nFast = 12, nSlow = 26, nSig = 9, maType = EMA),
    macd = macd_full[, "macd"],
    signal = macd_full[, "signal"],
    sma_14 = SMA(close, n = 14),
    macd_hist = macd - signal
  ) %>%
  mutate(
    bb = BBands(close, n = 20, sd = 2),
    bb_up = bb[, "up"],
    bb_dn = bb[, "dn"],
    bb_mavg = bb[, "mavg"],
    bb_pctb  = (close - bb_dn) / (bb_up - bb_dn)
  ) %>%
  select(-macd_full, -bb) %>%
  drop_na()

6. Podział na dane treningowe i testowe

splits <- brent_prices %>% time_series_split(assess = "1 month", cumulative = TRUE)
Using date_var: date
splits %>% tk_time_series_cv_plan() %>% plot_time_series_cv_plan(date, close)

7. Modelowanie

model_prophet <- prophet_reg(mode = "regression", growth = "linear", season = "additive") %>%
  set_engine("prophet", weekly.seasonality = TRUE) %>%
  fit(close ~ date, training(splits))
Ostrzeżenie: The following arguments cannot be manually modified and were removed: weekly.seasonality.Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
model_prophet_with_dxy <- prophet_reg(mode = "regression", growth = "linear", season = "additive") %>%
  set_engine("prophet", weekly.seasonality = TRUE) %>%
  fit(close ~ date + dxy, training(splits))
Ostrzeżenie: The following arguments cannot be manually modified and were removed: weekly.seasonality.Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
model_prophet_with_MACD <- prophet_reg(mode = "regression", growth = "linear", season = "additive") %>%
  set_engine("prophet", weekly.seasonality = TRUE) %>%
  fit(close ~ date + dxy + macd_hist, training(splits))
Ostrzeżenie: The following arguments cannot be manually modified and were removed: weekly.seasonality.Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
model_prophet_with_SMA14 <- prophet_reg(mode = "regression", growth = "linear", season = "additive") %>%
  set_engine("prophet", weekly.seasonality = TRUE) %>%
  fit(close ~ date + dxy + sma_14, training(splits))
Ostrzeżenie: The following arguments cannot be manually modified and were removed: weekly.seasonality.Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
model_prophet_with_MACD_SMA <- prophet_reg(mode = "regression", growth = "linear", season = "additive") %>%
  set_engine("prophet", weekly.seasonality = TRUE) %>%
  fit(close ~ date + dxy + macd_hist + sma_14, training(splits))
Ostrzeżenie: The following arguments cannot be manually modified and were removed: weekly.seasonality.Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
model_prophet_with_bb <- prophet_reg(mode = "regression", growth = "linear", season = "additive") %>%
  set_engine("prophet", weekly.seasonality = TRUE) %>%
  fit(close ~ date + dxy + macd_hist + sma_14 + bb_pctb, training(splits))
Ostrzeżenie: The following arguments cannot be manually modified and were removed: weekly.seasonality.Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
model_arima <- arima_reg(mode = "regression", seasonal_period = 12) %>%
  set_engine("auto_arima", stepwise = FALSE, approximation = FALSE) %>%
  fit(close ~ date, training(splits))

model_knn <- nearest_neighbor(mode = "regression", neighbors = 5) %>%
  set_engine("kknn") %>%
  fit(close ~ date, training(splits))

8. Symulacja przyszłych wartości zmiennych

simulate_regressor_prophet <- function(data, future_dates, reg_name, periods = NULL) {
  if (is.null(periods)) { periods <- nrow(future_dates) }
  reg_data <- data %>% select(ds = date, y = !!sym(reg_name)) %>% drop_na()
  model <- prophet(reg_data, daily.seasonality = TRUE)
  future <- make_future_dataframe(model, periods = periods)
  forecast <- predict(model, future)
  forecast_tail <- tail(forecast$yhat, periods)
  return(forecast_tail)
}

future_dates <- tibble(date = seq.Date(from = max(brent_prices$date) + 1, by = "day", length.out = 30))

simulated_dxy <- simulate_regressor_prophet(brent_prices, future_dates, "dxy")
simulated_macd <- simulate_regressor_prophet(brent_prices, future_dates, "macd_hist")
simulated_sma14 <- simulate_regressor_prophet(brent_prices, future_dates, "sma_14")

future_data <- future_dates %>%
  mutate(dxy = simulated_dxy, macd_hist = simulated_macd, sma_14 = simulated_sma14)

9. Kalibracja i porównanie modeli

models_table <- modeltime_table(
  model_prophet,
  model_arima,
  model_prophet_with_dxy,
  model_prophet_with_MACD,
  model_prophet_with_SMA14,
  model_prophet_with_MACD_SMA,
  model_prophet_with_bb,
  model_knn
)

models_table <- update_model_description(models_table, 3, "Prophet + DXY")
models_table <- update_model_description(models_table, 4, "Prophet + DXY + MACD")
models_table <- update_model_description(models_table, 5, "Prophet + DXY + SMA14")
models_table <- update_model_description(models_table, 6, "Prophet + DXY + MACD + SMA14")
models_table <- update_model_description(models_table, 7, "Prophet + DXY + MACD + SMA14 + BB")

calibration_table <- models_table %>%
  modeltime_calibrate(testing(splits))

calibration_table %>%
  modeltime_accuracy() %>%
  table_modeltime_accuracy()
Ostrzeżenie: There was 1 warning in `dplyr::mutate()`.
ℹ In argument: `.nested.col = purrr::map(...)`.
Caused by warning:
! A correlation computation is required, but `estimate` is constant and has 0 standard deviation, resulting in a divide by
0 error. `NA` will be returned.
library(kableExtra)

# Tworzymy accuracy BEZ usuwania kolumn
acc_table <- calibration_table %>%
  modeltime_accuracy()
Ostrzeżenie: There was 1 warning in `dplyr::mutate()`.
ℹ In argument: `.nested.col = purrr::map(...)`.
Caused by warning:
! A correlation computation is required, but `estimate` is constant and has 0 standard deviation, resulting in a divide by
0 error. `NA` will be returned.
library(kableExtra)

# Tworzymy accuracy BEZ usuwania kolumn
acc_table <- calibration_table %>%
  modeltime_accuracy()
Ostrzeżenie: There was 1 warning in `dplyr::mutate()`.
ℹ In argument: `.nested.col = purrr::map(...)`.
Caused by warning:
! A correlation computation is required, but `estimate` is constant and has 0 standard deviation, resulting in a divide by
0 error. `NA` will be returned.
# Ładna tabelka z wyśrodkowanym tytułem i widocznymi liniami
acc_table %>%
  knitr::kable(
    digits = 3,
    caption = "<center><strong>Podsumowanie wyników modeli - Miary dokładności</strong></center>",
    format = "html"
  ) %>%
  kable_styling(
    full_width = FALSE,
    position = "center",
    font_size = 14,
    bootstrap_options = c("striped", "hover"),
    fixed_thead = TRUE  # Wyrównanie nagłówka
  ) %>%
  row_spec(0, bold = TRUE, background = "#f2f2f2") %>%
  row_spec(1:nrow(acc_table), extra_css = "border-bottom: 2px solid black;") %>%
  column_spec(1:ncol(acc_table), extra_css = "border-right: 1px solid black; border-left: 1px solid black;") %>%
  column_spec(1, width = "1in")  # Wyrównanie szerokości pierwszej kolumny
Podsumowanie wyników modeli - Miary dokładności
.model_id .model_desc .type mae mape mase smape rmse rsq
1 PROPHET Test 10.261 15.598 6.779 14.255 11.220 0.728
2 ARIMA(0,1,5) Test 6.601 10.068 4.361 9.465 7.398 0.449
3 Prophet + DXY Test 9.674 14.713 6.391 13.506 10.606 0.784
4 Prophet + DXY + MACD Test 9.523 14.440 6.291 13.315 10.239 0.023
5 Prophet + DXY + SMA14 Test 4.091 6.247 2.703 5.937 5.324 0.051
6 Prophet + DXY + MACD + SMA14 Test 1.705 2.580 1.127 2.526 2.377 0.713
7 Prophet + DXY + MACD + SMA14 + BB Test 1.528 2.278 1.009 2.267 1.689 0.850
8 KKNN Test 6.331 9.647 4.182 9.100 7.054 NA
NA
NA
NA

Wyjaśnienie metryk

Oceny wydajności predykcyjnej zastosowanych modeli dokonano w oparciu o następujące metryki:

  • Średni Błąd Bezwzględny (MAE): Mierzy średnią wartość bezwzględnych różnic między rzeczywistymi a prognozowanymi wartościami.

    Wzór: \[ MAE = \frac{1}{n} \sum_{t=1}^{n} |y_t - \hat{y}_t| \]

  • Średni Procentowy Błąd Bezwzględny (MAPE): Oblicza średnią procentową różnicę między rzeczywistymi a prognozowanymi wartościami.

    Wzór: \[ MAPE = \frac{1}{n} \sum_{t=1}^{n} \left| \frac{y_t - \hat{y}_t}{y_t} \right| \times 100 \]

  • Skalowany Średni Błąd Bezwzględny (MASE): Skaluje błąd w stosunku do błędu naiwnego modelu.

    Wzór: \[ MASE = \frac{MAE}{\frac{1}{n-1} \sum_{t=2}^{n} |y_t - y_{t-1}|} \]

  • Symetryczny Średni Procentowy Błąd Bezwzględny (SMAPE): Jest to wersja MAPE, która uwzględnia symetrię w obliczaniu błędów.

    Wzór: \[ SMAPE = \frac{1}{n} \sum_{t=1}^{n} \frac{|y_t - \hat{y}_t|}{\frac{|y_t| + |\hat{y}_t|}{2}} \times 100 \]

  • Pierwiastek Średniego Błędu Kwadratowego (RMSE): Mierzy rozrzut błędów prognozy, biorąc pod uwagę kwadraty różnic.

    Wzór: \[ RMSE = \sqrt{\frac{1}{n} \sum_{t=1}^{n} (y_t - \hat{y}_t)^2} \]

  • Współczynnik Determinacji (R²): Mierzy, jak dobrze model wyjaśnia zmienność danych.

    Wzór: \[ R^2 = 1 - \frac{\sum_{t=1}^{n} (y_t - \hat{y}_t)^2}{\sum_{t=1}^{n} (y_t - \bar{y})^2} \]

10. Prognoza

Testowy forecast

calibration_table %>%
  modeltime_forecast(new_data = testing(splits), actual_data = brent_prices) %>%
  plot_modeltime_forecast(.interactive = TRUE)

Finalny forecast


11. Podsumowanie


12. Dalsze kroki


LS0tDQp0aXRsZTogIlByb2dub3phIHd5Y2VueSByb3B5IEJyZW50Ig0KYXV0aG9yOiAiS3J5c3RpYW4gJiBQb2xhIg0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpDQpsaWJyYXJ5KFF1YW5kbCkNCmxpYnJhcnkobHVicmlkYXRlKQ0KbGlicmFyeSh0aW1ldGspDQpsaWJyYXJ5KHBsb3RseSkNCmxpYnJhcnkoZ2djb3JycGxvdCkNCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeSh0aWR5cXVhbnQpDQpsaWJyYXJ5KGRhdGEudGFibGUpDQpsaWJyYXJ5KHRpZHltb2RlbHMpDQpsaWJyYXJ5KG1vZGVsdGltZSkNCmxpYnJhcnkocHJvcGhldCkNCmxpYnJhcnkoanNvbmxpdGUpDQpsaWJyYXJ5KHRzZmtubikNCmxpYnJhcnkoa2tubikNCmxpYnJhcnkoVFRSKQ0KbGlicmFyeSh0c2VyaWVzKQ0KYGBgDQoNCiMgMS4gV3Byb3dhZHplbmllDQoNClcgcmFtYWNoIG5pbmllanN6ZWogYW5hbGl6eSBwcm9nbm96dWplbXkgY2VueSByb3B5IEJyZW50IHByenkgdcW8eWNpdSByw7PFvG55Y2ggbWV0b2QgbW9kZWxvd2FuaWEgc3plcmVnw7N3IGN6YXNvd3ljaC4NCg0KKipBUklNQSAoQXV0b1JlZ3Jlc3NpdmUgSW50ZWdyYXRlZCBNb3ZpbmcgQXZlcmFnZSkqKiB0byBrbGFzeWN6bnkgbW9kZWwgc3RhdHlzdHljem55IHXFvHl3YW55IGRvIGFuYWxpenkgaSBwcm9nbm96b3dhbmlhIGRhbnljaCBzemVyZWfDs3cgY3phc293eWNoLg0KDQpNb2RlbCBBUklNQSDFgsSFY3p5IHRyenkga29tcG9uZW50eToNCg0KLSAgICoqQXV0b1JlZ3Jlc2rEmSAoQVIpKio6IHd5a29yenlzdGFuaWUgemFsZcW8bm/Fm2NpIG1pxJlkenkgYWt0dWFsbsSFIG9ic2Vyd2FjasSFIGEgamVqIHByemVzesWCeW1pIHdhcnRvxZtjaWFtaSwNCg0KLSAgICoqSW50ZWdyYWNqxJkgKEkpKio6IHN0YWJpbGl6YWNqxJkgc3plcmVndSBjemFzb3dlZ28gcG9wcnpleiByw7PFvG5pY293YW5pZSAodXN1d2FuaWUgdHJlbmR1KSwNCg0KLSAgICoqxZpyZWRuacSFIHJ1Y2hvbcSFIChNQSkqKjogbW9kZWxvd2FuaWUgYsWCxJlkdSBwcm9nbm96eSBqYWtvIGtvbWJpbmFjamkgYsWCxJlkw7N3IHogcHJ6ZXN6xYJvxZtjaS4NCg0KKipQcm9waGV0KiogdG8gbW9kZWwgb3ByYWNvd2FueSBwcnpleiBGYWNlYm9va2EsIHphcHJvamVrdG93YW55IHNwZWNqYWxuaWUgeiBtecWbbMSFIG8gcHJvZ25vem93YW5pdSBkYW55Y2ggeiB3eXJhxbpuxIUgc2V6b25vd2/Fm2NpxIUgZHppZW5uxIUsIHR5Z29kbmlvd8SFIGkgcm9jem7EhS4gUHJvcGhldCBqZXN0IG9kcG9ybnkgbmEgYnJha3VqxIVjZSBkYW5lIGkgbmlldHlwb3dlIHdhcnRvxZtjaSAob3V0bGllcnkpIG9yYXogdW1vxbxsaXdpYSB1d3pnbMSZZG5pYW5pZSBkb2RhdGtvd3ljaCB6bWllbm55Y2ggd3lqYcWbbmlhasSFY3ljaCAocmVncmVzb3LDs3cpIG9yYXogxZt3acSFdCAoaG9saWRheXMpLg0KDQohW0tvbXBvbmVudHkgUHJvcGhldGFdKGltYWdlcy9pbWFnZS5wbmcpDQoNClcgcHJvamVrY2llIHd5a29yenlzdGFubyBtLmluLiBtb2RlbGUgUHJvcGhldCwgUHJvcGhldCB6IGRvZGF0a293eW1pIHptaWVubnltaSwgQVJJTUEgb3JheiBLTk4uIE9jZW5pb25vIGljaCBkb2vFgmFkbm/Fm8SHIGkgcHJ6ZXByb3dhZHpvbm8gcHJlZHlrY2rEmSBuYSBwcnp5c3rFgmUgMzAgZG5pLg0KDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0KIyAyLiBQb2JyYW5pZSBkYW55Y2gNCg0KYGBge3J9DQpicmVudF9wcmljZXMgPC0gdHFfZ2V0KCJCWj1GIikNCmJyZW50X3ByaWNlcyAlPiUgcGxvdF90aW1lX3NlcmllcyhkYXRlLCBjbG9zZSwgLnNtb290aCA9IFQsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLmludGVyYWN0aXZlID0gVCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAudGl0bGUgPSAiQnJlbnQgT2lsIHByaWNlIChVU0QpIikNCmBgYA0KDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0KIyAzLiBEb2RhbmllIHptaWVubnljaCBvYmphxZtuaWFqxIVjeWNoDQoNCmBgYHtyfQ0KZG9sYXJfaW5kZXggPC0gdHFfZ2V0KCJEWC1ZLk5ZQiIpDQoNCmJyZW50X3ByaWNlcyA8LSBicmVudF9wcmljZXMgJT4lIA0KICBtdXRhdGUoZGF0ZSA9IGFzLkRhdGUoZGF0ZSkpICU+JQ0KICBsZWZ0X2pvaW4oZG9sYXJfaW5kZXggJT4lIG11dGF0ZShkYXRlID0gYXMuRGF0ZShkYXRlKSksIGJ5ID0gImRhdGUiKSAlPiUNCiAgc2VsZWN0KGRhdGUsIGNsb3NlID0gY2xvc2UueCwgZHh5ID0gY2xvc2UueSkgJT4lDQogIGRyb3BfbmEoKQ0KYGBgDQoNCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KDQojIDQuIFN0YWNqb25hcm5vxZvEhyBzemVyZWd1DQoNCmBgYHtyfQ0KYWRmLnRlc3QoYnJlbnRfcHJpY2VzJGNsb3NlLCBhbHRlcm5hdGl2ZSA9ICJzdGF0aW9uYXJ5IikNCmBgYA0KDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0KIyA1LiBJbsW8eW5pZXJpYSBjZWNoOiBhbmFsaXphIHRlY2huaWN6bmENCg0KYGBge3J9DQpicmVudF9wcmljZXMgPC0gYnJlbnRfcHJpY2VzICU+JQ0KICBhcnJhbmdlKGRhdGUpICU+JQ0KICBtdXRhdGUoDQogICAgbWFjZF9mdWxsID0gTUFDRChjbG9zZSwgbkZhc3QgPSAxMiwgblNsb3cgPSAyNiwgblNpZyA9IDksIG1hVHlwZSA9IEVNQSksDQogICAgbWFjZCA9IG1hY2RfZnVsbFssICJtYWNkIl0sDQogICAgc2lnbmFsID0gbWFjZF9mdWxsWywgInNpZ25hbCJdLA0KICAgIHNtYV8xNCA9IFNNQShjbG9zZSwgbiA9IDE0KSwNCiAgICBtYWNkX2hpc3QgPSBtYWNkIC0gc2lnbmFsDQogICkgJT4lDQogIG11dGF0ZSgNCiAgICBiYiA9IEJCYW5kcyhjbG9zZSwgbiA9IDIwLCBzZCA9IDIpLA0KICAgIGJiX3VwID0gYmJbLCAidXAiXSwNCiAgICBiYl9kbiA9IGJiWywgImRuIl0sDQogICAgYmJfbWF2ZyA9IGJiWywgIm1hdmciXSwNCiAgICBiYl9wY3RiICA9IChjbG9zZSAtIGJiX2RuKSAvIChiYl91cCAtIGJiX2RuKQ0KICApICU+JQ0KICBzZWxlY3QoLW1hY2RfZnVsbCwgLWJiKSAlPiUNCiAgZHJvcF9uYSgpDQpgYGANCg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCiMgNi4gUG9kemlhxYIgbmEgZGFuZSB0cmVuaW5nb3dlIGkgdGVzdG93ZQ0KDQpgYGB7cn0NCnNwbGl0cyA8LSBicmVudF9wcmljZXMgJT4lIHRpbWVfc2VyaWVzX3NwbGl0KGFzc2VzcyA9ICIxIG1vbnRoIiwgY3VtdWxhdGl2ZSA9IFRSVUUpDQpzcGxpdHMgJT4lIHRrX3RpbWVfc2VyaWVzX2N2X3BsYW4oKSAlPiUgcGxvdF90aW1lX3Nlcmllc19jdl9wbGFuKGRhdGUsIGNsb3NlKQ0KYGBgDQoNCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KDQojIDcuIE1vZGVsb3dhbmllDQoNCmBgYHtyfQ0KbW9kZWxfcHJvcGhldCA8LSBwcm9waGV0X3JlZyhtb2RlID0gInJlZ3Jlc3Npb24iLCBncm93dGggPSAibGluZWFyIiwgc2Vhc29uID0gImFkZGl0aXZlIikgJT4lDQogIHNldF9lbmdpbmUoInByb3BoZXQiLCB3ZWVrbHkuc2Vhc29uYWxpdHkgPSBUUlVFKSAlPiUNCiAgZml0KGNsb3NlIH4gZGF0ZSwgdHJhaW5pbmcoc3BsaXRzKSkNCg0KbW9kZWxfcHJvcGhldF93aXRoX2R4eSA8LSBwcm9waGV0X3JlZyhtb2RlID0gInJlZ3Jlc3Npb24iLCBncm93dGggPSAibGluZWFyIiwgc2Vhc29uID0gImFkZGl0aXZlIikgJT4lDQogIHNldF9lbmdpbmUoInByb3BoZXQiLCB3ZWVrbHkuc2Vhc29uYWxpdHkgPSBUUlVFKSAlPiUNCiAgZml0KGNsb3NlIH4gZGF0ZSArIGR4eSwgdHJhaW5pbmcoc3BsaXRzKSkNCg0KbW9kZWxfcHJvcGhldF93aXRoX01BQ0QgPC0gcHJvcGhldF9yZWcobW9kZSA9ICJyZWdyZXNzaW9uIiwgZ3Jvd3RoID0gImxpbmVhciIsIHNlYXNvbiA9ICJhZGRpdGl2ZSIpICU+JQ0KICBzZXRfZW5naW5lKCJwcm9waGV0Iiwgd2Vla2x5LnNlYXNvbmFsaXR5ID0gVFJVRSkgJT4lDQogIGZpdChjbG9zZSB+IGRhdGUgKyBkeHkgKyBtYWNkX2hpc3QsIHRyYWluaW5nKHNwbGl0cykpDQoNCm1vZGVsX3Byb3BoZXRfd2l0aF9TTUExNCA8LSBwcm9waGV0X3JlZyhtb2RlID0gInJlZ3Jlc3Npb24iLCBncm93dGggPSAibGluZWFyIiwgc2Vhc29uID0gImFkZGl0aXZlIikgJT4lDQogIHNldF9lbmdpbmUoInByb3BoZXQiLCB3ZWVrbHkuc2Vhc29uYWxpdHkgPSBUUlVFKSAlPiUNCiAgZml0KGNsb3NlIH4gZGF0ZSArIGR4eSArIHNtYV8xNCwgdHJhaW5pbmcoc3BsaXRzKSkNCg0KbW9kZWxfcHJvcGhldF93aXRoX01BQ0RfU01BIDwtIHByb3BoZXRfcmVnKG1vZGUgPSAicmVncmVzc2lvbiIsIGdyb3d0aCA9ICJsaW5lYXIiLCBzZWFzb24gPSAiYWRkaXRpdmUiKSAlPiUNCiAgc2V0X2VuZ2luZSgicHJvcGhldCIsIHdlZWtseS5zZWFzb25hbGl0eSA9IFRSVUUpICU+JQ0KICBmaXQoY2xvc2UgfiBkYXRlICsgZHh5ICsgbWFjZF9oaXN0ICsgc21hXzE0LCB0cmFpbmluZyhzcGxpdHMpKQ0KDQptb2RlbF9wcm9waGV0X3dpdGhfYmIgPC0gcHJvcGhldF9yZWcobW9kZSA9ICJyZWdyZXNzaW9uIiwgZ3Jvd3RoID0gImxpbmVhciIsIHNlYXNvbiA9ICJhZGRpdGl2ZSIpICU+JQ0KICBzZXRfZW5naW5lKCJwcm9waGV0Iiwgd2Vla2x5LnNlYXNvbmFsaXR5ID0gVFJVRSkgJT4lDQogIGZpdChjbG9zZSB+IGRhdGUgKyBkeHkgKyBtYWNkX2hpc3QgKyBzbWFfMTQgKyBiYl9wY3RiLCB0cmFpbmluZyhzcGxpdHMpKQ0KDQptb2RlbF9hcmltYSA8LSBhcmltYV9yZWcobW9kZSA9ICJyZWdyZXNzaW9uIiwgc2Vhc29uYWxfcGVyaW9kID0gMTIpICU+JQ0KICBzZXRfZW5naW5lKCJhdXRvX2FyaW1hIiwgc3RlcHdpc2UgPSBGQUxTRSwgYXBwcm94aW1hdGlvbiA9IEZBTFNFKSAlPiUNCiAgZml0KGNsb3NlIH4gZGF0ZSwgdHJhaW5pbmcoc3BsaXRzKSkNCg0KbW9kZWxfa25uIDwtIG5lYXJlc3RfbmVpZ2hib3IobW9kZSA9ICJyZWdyZXNzaW9uIiwgbmVpZ2hib3JzID0gNSkgJT4lDQogIHNldF9lbmdpbmUoImtrbm4iKSAlPiUNCiAgZml0KGNsb3NlIH4gZGF0ZSwgdHJhaW5pbmcoc3BsaXRzKSkNCmBgYA0KDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0KIyA4LiBTeW11bGFjamEgcHJ6eXN6xYJ5Y2ggd2FydG/Fm2NpIHptaWVubnljaA0KDQpgYGB7cn0NCnNpbXVsYXRlX3JlZ3Jlc3Nvcl9wcm9waGV0IDwtIGZ1bmN0aW9uKGRhdGEsIGZ1dHVyZV9kYXRlcywgcmVnX25hbWUsIHBlcmlvZHMgPSBOVUxMKSB7DQogIGlmIChpcy5udWxsKHBlcmlvZHMpKSB7IHBlcmlvZHMgPC0gbnJvdyhmdXR1cmVfZGF0ZXMpIH0NCiAgcmVnX2RhdGEgPC0gZGF0YSAlPiUgc2VsZWN0KGRzID0gZGF0ZSwgeSA9ICEhc3ltKHJlZ19uYW1lKSkgJT4lIGRyb3BfbmEoKQ0KICBtb2RlbCA8LSBwcm9waGV0KHJlZ19kYXRhLCBkYWlseS5zZWFzb25hbGl0eSA9IFRSVUUpDQogIGZ1dHVyZSA8LSBtYWtlX2Z1dHVyZV9kYXRhZnJhbWUobW9kZWwsIHBlcmlvZHMgPSBwZXJpb2RzKQ0KICBmb3JlY2FzdCA8LSBwcmVkaWN0KG1vZGVsLCBmdXR1cmUpDQogIGZvcmVjYXN0X3RhaWwgPC0gdGFpbChmb3JlY2FzdCR5aGF0LCBwZXJpb2RzKQ0KICByZXR1cm4oZm9yZWNhc3RfdGFpbCkNCn0NCg0KZnV0dXJlX2RhdGVzIDwtIHRpYmJsZShkYXRlID0gc2VxLkRhdGUoZnJvbSA9IG1heChicmVudF9wcmljZXMkZGF0ZSkgKyAxLCBieSA9ICJkYXkiLCBsZW5ndGgub3V0ID0gMzApKQ0KDQpzaW11bGF0ZWRfZHh5IDwtIHNpbXVsYXRlX3JlZ3Jlc3Nvcl9wcm9waGV0KGJyZW50X3ByaWNlcywgZnV0dXJlX2RhdGVzLCAiZHh5IikNCnNpbXVsYXRlZF9tYWNkIDwtIHNpbXVsYXRlX3JlZ3Jlc3Nvcl9wcm9waGV0KGJyZW50X3ByaWNlcywgZnV0dXJlX2RhdGVzLCAibWFjZF9oaXN0IikNCnNpbXVsYXRlZF9zbWExNCA8LSBzaW11bGF0ZV9yZWdyZXNzb3JfcHJvcGhldChicmVudF9wcmljZXMsIGZ1dHVyZV9kYXRlcywgInNtYV8xNCIpDQoNCmZ1dHVyZV9kYXRhIDwtIGZ1dHVyZV9kYXRlcyAlPiUNCiAgbXV0YXRlKGR4eSA9IHNpbXVsYXRlZF9keHksIG1hY2RfaGlzdCA9IHNpbXVsYXRlZF9tYWNkLCBzbWFfMTQgPSBzaW11bGF0ZWRfc21hMTQpDQpgYGANCg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCiMgOS4gS2FsaWJyYWNqYSBpIHBvcsOzd25hbmllIG1vZGVsaQ0KDQpgYGB7cn0NCm1vZGVsc190YWJsZSA8LSBtb2RlbHRpbWVfdGFibGUoDQogIG1vZGVsX3Byb3BoZXQsDQogIG1vZGVsX2FyaW1hLA0KICBtb2RlbF9wcm9waGV0X3dpdGhfZHh5LA0KICBtb2RlbF9wcm9waGV0X3dpdGhfTUFDRCwNCiAgbW9kZWxfcHJvcGhldF93aXRoX1NNQTE0LA0KICBtb2RlbF9wcm9waGV0X3dpdGhfTUFDRF9TTUEsDQogIG1vZGVsX3Byb3BoZXRfd2l0aF9iYiwNCiAgbW9kZWxfa25uDQopDQoNCm1vZGVsc190YWJsZSA8LSB1cGRhdGVfbW9kZWxfZGVzY3JpcHRpb24obW9kZWxzX3RhYmxlLCAzLCAiUHJvcGhldCArIERYWSIpDQptb2RlbHNfdGFibGUgPC0gdXBkYXRlX21vZGVsX2Rlc2NyaXB0aW9uKG1vZGVsc190YWJsZSwgNCwgIlByb3BoZXQgKyBEWFkgKyBNQUNEIikNCm1vZGVsc190YWJsZSA8LSB1cGRhdGVfbW9kZWxfZGVzY3JpcHRpb24obW9kZWxzX3RhYmxlLCA1LCAiUHJvcGhldCArIERYWSArIFNNQTE0IikNCm1vZGVsc190YWJsZSA8LSB1cGRhdGVfbW9kZWxfZGVzY3JpcHRpb24obW9kZWxzX3RhYmxlLCA2LCAiUHJvcGhldCArIERYWSArIE1BQ0QgKyBTTUExNCIpDQptb2RlbHNfdGFibGUgPC0gdXBkYXRlX21vZGVsX2Rlc2NyaXB0aW9uKG1vZGVsc190YWJsZSwgNywgIlByb3BoZXQgKyBEWFkgKyBNQUNEICsgU01BMTQgKyBCQiIpDQoNCmNhbGlicmF0aW9uX3RhYmxlIDwtIG1vZGVsc190YWJsZSAlPiUNCiAgbW9kZWx0aW1lX2NhbGlicmF0ZSh0ZXN0aW5nKHNwbGl0cykpDQoNCmNhbGlicmF0aW9uX3RhYmxlICU+JQ0KICBtb2RlbHRpbWVfYWNjdXJhY3koKSAlPiUNCiAgdGFibGVfbW9kZWx0aW1lX2FjY3VyYWN5KCkNCg0KDQoNCmxpYnJhcnkoa2FibGVFeHRyYSkNCg0KDQphY2NfdGFibGUgPC0gY2FsaWJyYXRpb25fdGFibGUgJT4lDQogIG1vZGVsdGltZV9hY2N1cmFjeSgpDQoNCiMgxYFhZG5hIHRhYmVsa2EgeiB3ecWbcm9ka293YW55bSB0eXR1xYJlbSBpIHdpZG9jem55bWkgbGluaWFtaQ0KYWNjX3RhYmxlICU+JQ0KICBrbml0cjo6a2FibGUoDQogICAgZGlnaXRzID0gMywNCiAgICBjYXB0aW9uID0gIjxjZW50ZXI+PHN0cm9uZz5Qb2RzdW1vd2FuaWUgd3luaWvDs3cgbW9kZWxpIC0gTWlhcnkgZG9rxYJhZG5vxZtjaTwvc3Ryb25nPjwvY2VudGVyPiIsDQogICAgZm9ybWF0ID0gImh0bWwiDQogICkgJT4lDQogIGthYmxlX3N0eWxpbmcoDQogICAgZnVsbF93aWR0aCA9IEZBTFNFLA0KICAgIHBvc2l0aW9uID0gImNlbnRlciIsDQogICAgZm9udF9zaXplID0gMTQsDQogICAgYm9vdHN0cmFwX29wdGlvbnMgPSBjKCJzdHJpcGVkIiwgImhvdmVyIiksDQogICAgZml4ZWRfdGhlYWQgPSBUUlVFICAjIFd5csOzd25hbmllIG5hZ8WCw7N3a2ENCiAgKSAlPiUNCiAgcm93X3NwZWMoMCwgYm9sZCA9IFRSVUUsIGJhY2tncm91bmQgPSAiI2YyZjJmMiIpICU+JQ0KICByb3dfc3BlYygxOm5yb3coYWNjX3RhYmxlKSwgZXh0cmFfY3NzID0gImJvcmRlci1ib3R0b206IDJweCBzb2xpZCBibGFjazsiKSAlPiUNCiAgY29sdW1uX3NwZWMoMTpuY29sKGFjY190YWJsZSksIGV4dHJhX2NzcyA9ICJib3JkZXItcmlnaHQ6IDFweCBzb2xpZCBibGFjazsgYm9yZGVyLWxlZnQ6IDFweCBzb2xpZCBibGFjazsiKSAlPiUNCiAgY29sdW1uX3NwZWMoMSwgd2lkdGggPSAiMWluIikgICMgV3lyw7N3bmFuaWUgc3plcm9rb8WbY2kgcGllcndzemVqIGtvbHVtbnkNCg0KDQoNCmBgYA0KDQojIyBXeWphxZtuaWVuaWUgbWV0cnlrDQoNCk9jZW55IHd5ZGFqbm/Fm2NpIHByZWR5a2N5am5laiB6YXN0b3Nvd2FueWNoIG1vZGVsaSBkb2tvbmFubyB3IG9wYXJjaXUgbyBuYXN0xJlwdWrEhWNlIG1ldHJ5a2k6DQoNCi0gICAqKsWacmVkbmkgQsWCxIVkIEJlend6Z2zEmWRueSAoTUFFKSoqOiBNaWVyenkgxZtyZWRuacSFIHdhcnRvxZvEhyBiZXp3emdsxJlkbnljaCByw7PFvG5pYyBtacSZZHp5IHJ6ZWN6eXdpc3R5bWkgYSBwcm9nbm96b3dhbnltaSB3YXJ0b8WbY2lhbWkuDQoNCiAgICBXesOzcjogJCQNCiAgICBNQUUgPSBcZnJhY3sxfXtufSBcc3VtX3t0PTF9XntufSB8eV90IC0gXGhhdHt5fV90fA0KICAgICQkDQoNCi0gICAqKsWacmVkbmkgUHJvY2VudG93eSBCxYLEhWQgQmV6d3pnbMSZZG55IChNQVBFKSoqOiBPYmxpY3phIMWbcmVkbmnEhSBwcm9jZW50b3fEhSByw7PFvG5pY8SZIG1pxJlkenkgcnplY3p5d2lzdHltaSBhIHByb2dub3pvd2FueW1pIHdhcnRvxZtjaWFtaS4NCg0KICAgIFd6w7NyOiAkJA0KICAgIE1BUEUgPSBcZnJhY3sxfXtufSBcc3VtX3t0PTF9XntufSBcbGVmdHwgXGZyYWN7eV90IC0gXGhhdHt5fV90fXt5X3R9IFxyaWdodHwgXHRpbWVzIDEwMA0KICAgICQkDQoNCi0gICAqKlNrYWxvd2FueSDFmnJlZG5pIELFgsSFZCBCZXp3emdsxJlkbnkgKE1BU0UpKio6IFNrYWx1amUgYsWCxIVkIHcgc3Rvc3Vua3UgZG8gYsWCxJlkdSBuYWl3bmVnbyBtb2RlbHUuDQoNCiAgICBXesOzcjogJCQNCiAgICBNQVNFID0gXGZyYWN7TUFFfXtcZnJhY3sxfXtuLTF9IFxzdW1fe3Q9Mn1ee259IHx5X3QgLSB5X3t0LTF9fH0NCiAgICAkJA0KDQotICAgKipTeW1ldHJ5Y3pueSDFmnJlZG5pIFByb2NlbnRvd3kgQsWCxIVkIEJlend6Z2zEmWRueSAoU01BUEUpKio6IEplc3QgdG8gd2Vyc2phIE1BUEUsIGt0w7NyYSB1d3pnbMSZZG5pYSBzeW1ldHJpxJkgdyBvYmxpY3phbml1IGLFgsSZZMOzdy4NCg0KICAgIFd6w7NyOiAkJA0KICAgIFNNQVBFID0gXGZyYWN7MX17bn0gXHN1bV97dD0xfV57bn0gXGZyYWN7fHlfdCAtIFxoYXR7eX1fdHx9e1xmcmFje3x5X3R8ICsgfFxoYXR7eX1fdHx9ezJ9fSBcdGltZXMgMTAwDQogICAgJCQNCg0KLSAgICoqUGllcndpYXN0ZWsgxZpyZWRuaWVnbyBCxYLEmWR1IEt3YWRyYXRvd2VnbyAoUk1TRSkqKjogTWllcnp5IHJvenJ6dXQgYsWCxJlkw7N3IHByb2dub3p5LCBiaW9yxIVjIHBvZCB1d2FnxJkga3dhZHJhdHkgcsOzxbxuaWMuDQoNCiAgICBXesOzcjogJCQNCiAgICBSTVNFID0gXHNxcnR7XGZyYWN7MX17bn0gXHN1bV97dD0xfV57bn0gKHlfdCAtIFxoYXR7eX1fdCleMn0NCiAgICAkJA0KDQotICAgKipXc3DDs8WCY3p5bm5payBEZXRlcm1pbmFjamkgKFLCsikqKjogTWllcnp5LCBqYWsgZG9icnplIG1vZGVsIHd5amHFm25pYSB6bWllbm5vxZvEhyBkYW55Y2guDQoNCiAgICBXesOzcjogJCQNCiAgICBSXjIgPSAxIC0gXGZyYWN7XHN1bV97dD0xfV57bn0gKHlfdCAtIFxoYXR7eX1fdCleMn17XHN1bV97dD0xfV57bn0gKHlfdCAtIFxiYXJ7eX0pXjJ9DQogICAgJCQNCg0KIyAxMC4gUHJvZ25vemENCg0KIyMgVGVzdG93eSBmb3JlY2FzdA0KDQpgYGB7cn0NCmNhbGlicmF0aW9uX3RhYmxlICU+JQ0KICBtb2RlbHRpbWVfZm9yZWNhc3QobmV3X2RhdGEgPSB0ZXN0aW5nKHNwbGl0cyksIGFjdHVhbF9kYXRhID0gYnJlbnRfcHJpY2VzKSAlPiUNCiAgcGxvdF9tb2RlbHRpbWVfZm9yZWNhc3QoLmludGVyYWN0aXZlID0gVFJVRSkNCmBgYA0KDQojIyBGaW5hbG55IGZvcmVjYXN0DQoNCmBgYHtyfQ0KY2FsaWJyYXRpb25fdGFibGUgJT4lDQogIG1vZGVsdGltZV9yZWZpdChicmVudF9wcmljZXMpICU+JQ0KICBtb2RlbHRpbWVfZm9yZWNhc3QobmV3X2RhdGEgPSBmdXR1cmVfZGF0YSwgYWN0dWFsX2RhdGEgPSBicmVudF9wcmljZXMpICU+JQ0KICBwbG90X21vZGVsdGltZV9mb3JlY2FzdCguaW50ZXJhY3RpdmUgPSBUUlVFLCAucGxvdGx5X3NsaWRlciA9IFRSVUUpDQpgYGANCg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCiMgMTEuIFBvZHN1bW93YW5pZQ0KDQotICAgTmFqbGVwc3plIHd5bmlraSB1enlza2FubyBkbGEgbW9kZWxpIFByb3BoZXQgeiBkb2RhdGtvd3ltaSB6bWllbm55bWkgdGVjaG5pY3pueW1pLg0KLSAgIE1vZGVsZSBBUklNQSBpIEtOTiBtaWHFgnkgd3nFvHN6xIUgd2FydG/Fm8SHIGLFgsSZZHUuDQotICAgRG9kYW5pZSB6bWllbm55Y2ggbWFrcm9la29ub21pY3pueWNoIChEb2xsYXIgSW5kZXgpIGkgdGVjaG5pY3pueWNoIChNQUNELCBTTUEsIEJvbGxpbmdlciBCYW5kcykgendpxJlrc3p5xYJvIHRyYWZub8WbxIcgcHJvZ25vei4NCg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCiMgMTIuIERhbHN6ZSBrcm9raQ0KDQotICAgUHJ6ZXRlc3Rvd2HEhyB3acSZY2VqIHptaWVubnljaCBtYWtyb2Vrb25vbWljem55Y2guDQotICAgUm96c3plcnp5xIcgYW5hbGl6xJkgbyBkxYJ1xbxzemUgaG9yeXpvbnR5IGN6YXNvd2UuDQotICAgWmFzdG9zb3dhxIcgaW5uZSBhbGdvcnl0bXkgKG5wLiBYR0Jvb3N0LCBMU1RNKS4NCg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQo=